package viper.receiver;

import java.util.NoSuchElementException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import java.io.IOException;

import javax.media.Manager;
import javax.media.NoPlayerException;
import javax.media.Player;
import javax.media.Time;
import javax.media.protocol.PushBufferDataSource;
import javax.media.protocol.PushBufferStream;


import viper.GUI.ViperGUI;
import viper.call.common.LiveStream;
import viper.diversity.Dediversity;
import viper.main.StateManager;
import viper.main.ViperHeader;

/**
 * @author Kevin Sin
 * 
 * Revision number: 5
 * Date created: 3/21/10
 * Date of Last Back-up on SVN: 3/21/10
 * 
 * <p>CallReceiver is used in calls for getting the
 * call receive packets, parsing them for the timestamps,
 * and putting them into the right place for the playback
 * buffer (if it is early enough). 
 */
public class CallReceiver implements Runnable {

	//-------------------Singleton structure----------------------
	//public final static CallReceiver INSTANCE = new CallReceiver();
	public CallReceiver(){
		
	}
	//-------------------------------------------------------------
	
	//Parsing variables-----------------------------
	private boolean readyToParse = false;

	public synchronized boolean isReadyToParse() {
		return readyToParse;
	}

	public void setReadyToParse(boolean readyToParse) {
		this.readyToParse = readyToParse;
	}
	//---------------------------------------------

	//Playback--------------------------------------
	private LiveStream rxls;
	private Player player = null;
	private CustomDataSource rxcds;
	//----------------------------------------------
	
	//Sliding window parameters---------------------
	TreeMap<Long, byte[][]> windowTree;
	long t_play; //marker for when to play
	private int initial_cushion; //wait this many ms from first packet
	long t_diff; //difference between system time and player, set once.
	byte[][] newByteArr;
	int divCodeLen;
	byte[] timeBytes = new byte[8];
	Long thisTimeLong = new Long(0);
	byte[][] timerByteArray;
	Long firstTime = new Long(0);
	
	private long putTime, getTime, lastTime;
	//-----------------------------------------------
	
	//Timer parameters--------------------------------
	private Timer timer;
	private int checking_period = 30;
	public boolean gotFirstPacket = false;
	public boolean timeoutmode = false;
	int timeouts = 200; //get a timeouts start over faster
	int TIMEOUTS_RESTART = 150;
	//-----------------------------------------------
	
	//Statistic Parameters---------------------
	private long ReceivedTotalPackets;
	private long ReceivedVoicePackets;
	private long SentTotalPackets;
	private long SentVoicePackets;
	private long RTC;
	private long RVC;
	private long STC;
	private long SVC;
	
	public void run() {

		lastTime = 0;
		
		timer = new Timer();
		rxls = new LiveStream();
		
		//determine size of buckets depending on type of diversity code used
		if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_101 || StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_111)
			divCodeLen = 1;
		else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_232)
			divCodeLen = 3;
		else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_364)
			divCodeLen = 6;
		
		
		//Initialize windowTree and timeout depending on div. code
		//byte divCode = StateManager.INSTANCE.getDiversityCode();
		windowTree = new TreeMap<Long, byte[][]>();
		
		t_play = Long.MAX_VALUE;
		initial_cushion = 200;
		
		try {		
			// Construct a new socket on specified port.
			// All packets received on this port will be assumed to be VIPER packets.
			// Construct the custom PushBufferStream
			// Initialize receive stream with DataSource
			rxcds = new CustomDataSource(rxls);
			rxcds.connect(); 

			// Construct and start the audio player
			player = Manager.createRealizedPlayer(rxcds);
		} catch (Throwable t) {
			t.printStackTrace();
		}
		
		readyToParse = true;
		
		System.out.println("run CallReceiver");
		
		System.out.println("Starting player...");
		//while(true){
			player.start();
		//}
	}
	
	public LiveStream getStream(){
		return rxls;
	}
	
	public void jumpstart(){
		if(player!=null){
			System.out.println("Jumpstarting Player.");
			try{
				// Construct and start the audio player
				player = Manager.createPlayer(rxcds);
				player.realize();
				player.start();
			}catch(IOException ioe){
				System.out.println("IOException while jumpstarting.");
				ioe.printStackTrace();
			}catch(NoPlayerException npe){
				System.out.println("NoPlayerException while jumpstarting.");
			}
		}
	}

	public void parsePacket(byte[] buff) 
	{
		if(!readyToParse) //player is not ready yet
		{
			return;
		}

		//get timestamp from header
		System.arraycopy(buff, 0, timeBytes, 0, 8);
		long timeStamp = ViperHeader.byteArrayToLong(timeBytes);
		/**
		System.out.println("=================================");
		System.out.println("Socket Receive Buffer: "+buff);
		System.out.println("Socket Receive TimeStamp: "+timeStamp*1000000);
		System.out.println("=================================");
		**/
		if(!gotFirstPacket) //use first packet to sync timer
		{
			//if we are timing out and need to grab a non-delayed reference frame
			if(timeoutmode){
				//only take a reference if it arrived on-time
				if(timeStamp >= lastTime){
					//System.out.println("DEBUG: should not get here more than once");
					t_play = timeStamp - initial_cushion;
					gotFirstPacket = true;
					timeoutmode = false;
					timer.schedule(new ViperTimerTask(), 0, checking_period);
				}
			}
			else{
				//System.out.println("DEBUG: should not get here more than once");
				t_play = timeStamp - initial_cushion;
				gotFirstPacket = true;
				timeoutmode = false;
				timer.schedule(new ViperTimerTask(), 0, checking_period);
			}
		}

		//parse the packet type for diversity
		byte pktType = buff[8];

		//get payload
		byte[] data = new byte[buff.length - 9];
		System.arraycopy(buff, 9, data, 0, data.length);

		//as long as we are not too late, put the packet in windowTree
		if(((thisTimeLong = timeStamp) > t_play) && (timeStamp >= lastTime))
		{
			//System.out.println("DEBUG: add: timestamp " + timeStamp + ", t_play " + t_play);
			
			//retrieve the bucket from hash map at specified pkt type index
			newByteArr = windowTree.get(thisTimeLong);
			
			//if nothing was there, then make a new bucket
			if(newByteArr==null){
				newByteArr = new byte[divCodeLen][];
				/** Debug-Ji
				System.out.println("Null "+divCodeLen);
				**/
			}
			
			//add necessary pkt data in bucket
			newByteArr[(int)pktType] = data;
			
			/** Debug-Ji
			 * System.out.println("PKT TYPE: "+(int)pktType);
			System.out.println("CHECKING STORED BYTE[][]: "+ 
			(newByteArr[0]==null) +" "+(newByteArr[1]==null)+" "+
			(newByteArr[2]==null));
			if(data!=null)
				System.out.println("NOT NULL AT RX");
			**/
			
			//replace bucket into hash map
			synchronized(windowTree)
			{
				windowTree.put(thisTimeLong, newByteArr);
				putTime = thisTimeLong;
			}
		}
	}

	/**
	 * @param partial
	 * @param bs
	 * @return
	 * 
	 * Calls Joe's diversity code.
	 */
	private byte[][] dediversify(boolean partial, byte[][] bs) {
		byte [][] temp = null;
		
		if(divCodeLen==3){
			temp = Dediversity.undiversify(bs[0], bs[1], bs[2]);
			for(int i = 0; i<2;i++){
			if(temp[i] != null){
				ReceivedVoicePackets++;
				RVC++;
			}
			}
			for(int i = 0; i<3;i++){
				if(bs[i] != null){
					ReceivedTotalPackets++;
					RTC++;
				}
				}
		}
		else if(divCodeLen==6){
			temp = Dediversity.undiversify(bs[0], bs[1], bs[2], bs[3], bs[4], bs[5]);
			for(int i = 0; i<3; i++){
			if(temp[i] != null){
				ReceivedVoicePackets++;
				RVC++;
			}
			}
			for(int i = 0; i<6;i++){
				if(bs[i] != null){
					ReceivedTotalPackets++;
					RTC++;
				}
				}
		}
		else if(divCodeLen==1){
			temp = bs; //just send to playback as is, there is no diversity code
			ReceivedVoicePackets++;
			ReceivedTotalPackets++;
			RVC++;
			RTC++;
		}
		else //default case for unknown diversity coding
			System.err.println("Unknown diversity coding detected during dediversification.");
		
		return temp;
	}

	/**
	 * Used when a call is ended.  Make sure all the variables are reset.
	 */
	public void end()
	{
		if(player!=null)
		{
			player.stop();
			player = null;
		}
		if(rxcds!=null)
		{
			try {
				rxcds.stop();
				rxcds = null;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}	
		timer.cancel();
		timer = null;
		rxls.stopTimer();
		rxls = null;
		readyToParse = false;
		gotFirstPacket = false;
		windowTree.clear();
		System.out.println("stop CallReceiver");
	}

	class CustomDataSource extends PushBufferDataSource {

		protected Object [] controls = new Object[0];
		protected boolean started = false;
		protected String contentType = "raw";
		protected boolean connected = false;
		protected Time duration = DURATION_UNKNOWN;
		protected LiveStream [] streams = null;
		protected LiveStream stream = null;

		public CustomDataSource(LiveStream ls) {
			streams = new LiveStream[1];
			streams[0] = ls;
			stream = ls;
		}

		public String getContentType() {
			if (!connected){
				System.err.println("Error: DataSource not connected");
				return null;
			}
			return contentType;
		}

		public void connect() throws IOException {
			if (connected)
				return;
			connected = true;
		}

		public void disconnect() {
			try {
				if (started)
					stop();
			} catch (IOException e) {}
			connected = false;
		}

		public void start() throws IOException {
			// we need to throw error if connect() has not been called
			if (!connected)
				throw new java.lang.Error("DataSource must be connected before it can be started");
			if (started)
				return;
			started = true;
			stream.start(true);
		}

		public void stop() throws IOException {
			if ((!connected) || (!started))
				return;
			started = false;
			stream.start(false);
		}

		public Object [] getControls() {
			return controls;
		}

		public Object getControl(String controlType) {
			try {
				Class<?>  cls = Class.forName(controlType);
				Object cs[] = getControls();
				for (int i = 0; i < cs.length; i++) {
					if (cls.isInstance(cs[i]))
						return cs[i];
				}
				return null;

			} catch (Exception e) {   // no such controlType or such control
				return null;
			}
		}

		public Time getDuration() {
			return duration;
		}
		
	

		public PushBufferStream [] getStreams() {
			if (streams == null) {
				streams = new LiveStream[1];
				stream = streams[0] = new LiveStream();
			}
			return streams;
		}
	}
	
	/**
	 * 
	 * @author Grace
	 * Sources:
	 * http://java.sun.com/products/jfc/tsc/articles/timer/
	 * http://java.sun.com/j2se/1.5.0/docs/api/java/util/TimerTask.html
	 */
	class ViperTimerTask extends TimerTask{
		
		int COUNT_MAX = 25;
		int countUnsuccessful = 0;
		
		public void run() {		
			//check current range
			//t_play = System.currentTimeMillis() - t_diff - initial_cushion;
			t_play = t_play + checking_period;
			timeouts++;
			playAvailable();
		}
		
		public void playAvailable()
		{
			try{
				if((firstTime = windowTree.firstKey()) < t_play)
				{
					if((timerByteArray = windowTree.get(firstTime))!=null)
					{
						//System.out.println("playing: " + firstTime + ", t_play " + t_play);
						byte[][] decoded = dediversify(false, timerByteArray);
						for(int i=0;i<decoded.length;i++)//for(byte[] d : decoded)
						{
							rxls.update(decoded[i],firstTime*1000000);
							countUnsuccessful=0;
							getTime = firstTime;
							lastTime = firstTime;
							if((putTime-getTime)>800)
								windowTree.clear();
						}
						
						synchronized(windowTree)
						{
							windowTree.remove(firstTime); //clean up if played something
						}
						playAvailable(); //recursive
					}
				}
			}
			catch(NoSuchElementException a) //no times in windowTree
			{
				countUnsuccessful++;
				//System.out.println(countUnsuccessful);
			}
			
			//reached limit for unsuccessful playbacks
			if(countUnsuccessful>COUNT_MAX)
			{
				synchronized(windowTree)
				{
					windowTree.clear();
				}
				timer.cancel();
				timer = new Timer();
				gotFirstPacket = false;
				timeoutmode = true;
				System.out.println("quick start over!");
				timeouts=0;
			}
			
			//periodic check
			if(timeouts>TIMEOUTS_RESTART)
			{
				timer.cancel();
				timer = new Timer();
				gotFirstPacket = false;
				timeoutmode = true;
				System.out.println("timeouts start over!");
				timeouts=0;
				
				//clear Player buffer
				getStream().clearBuffer();
			}
		}
	}

	public long[] getStatistics(){
		long[] stats = new long[8];
		stats[0] = ReceivedTotalPackets;
		stats[1] = SentTotalPackets;
		stats[2] = ReceivedVoicePackets;
		stats[3] = SentVoicePackets;
		stats[4]= RTC;
		stats[5] = STC;
		stats[6]= RVC;
		stats[7] = SVC;
		RTC=0;
		STC=0;
		RVC=0;
		SVC=0;
		
		/*
		ReceivedTotalPackets = 0;
		ReceivedVoicePackets = 0;
		SentTotalPackets = 0;
		SentVoicePackets = 0;
		*/
		return stats;
	}
	
	public void setPacketsSent(long[] templong) {
		 SentTotalPackets+=templong[0];
		 SentVoicePackets+=templong[1];	
		 STC = templong[0];
		 SVC = templong[1];

		 //update GUI
		 ViperGUI.INSTANCE.getInCallPanel().updateStatsInFrame();
	}
	
	public void clearStats() {
		SentTotalPackets = 0;
		SentVoicePackets = 0;
		
		//update GUI
		ViperGUI.INSTANCE.getInCallPanel().updateStatsInFrame();
	}
}